package com.gxar.file.server.base.sdk;

import com.alibaba.fastjson.JSON;
import com.aliyun.oss.model.PartETag;
import com.gxar.fusion.redis.service.RedisService;
import com.google.common.collect.Lists;
import com.gxar.common.exception.APIRuntimeException;
import com.gxar.file.server.base.common.constant.FileConstant;
import com.gxar.file.server.base.common.enums.ProductEnum;
import com.gxar.file.server.base.common.enums.TargetChannelEnum;
import com.gxar.file.server.base.model.ResultCodeEnum;
import com.gxar.file.server.base.model.upload.request.FilePartUploadRequest;
import com.gxar.file.server.base.model.upload.response.FilePartUploadResponse;
import com.gxar.file.server.db.model.ProductEntity;
import com.gxar.file.server.db.respository.ProductRepository;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Component
@Configuration
@RequiredArgsConstructor
public class MinIoHelper {

    public static final Logger log = LoggerFactory.getLogger(MinIoHelper.class);

    private final static Map<String, MinioClient> OSS_CLIENT_MAP = new ConcurrentHashMap<>();
    private final static Map<String, ProductEntity> PRODUCT_MAP = new ConcurrentHashMap<>();
    private final static Integer MIN_IO_PORT = 9090;

    private final ProductRepository productRepository;

    private final RedisService redisRepository;

    private synchronized MinioClient getMinioClient(String productName) {
        MinioClient ossClient = OSS_CLIENT_MAP.get(productName);
        if (ossClient != null) {
            return ossClient;
        } else {
            ProductEntity productEntity = productRepository.findByNameAndChannel(productName, TargetChannelEnum.MIN_IO.getCode());
            if (Objects.nonNull(productEntity)) {
                PRODUCT_MAP.put(productName, productEntity);
                ossClient = MinioClient.builder().endpoint(productEntity.getEndpoint(), MIN_IO_PORT, false).credentials(productEntity.getAccessKey(), productEntity.getAccessSecret()).build();
                OSS_CLIENT_MAP.put(productName, ossClient);
                return ossClient;
            } else {
                return null;
            }
        }
    }


    /**
     * 创建bucket
     *
     * @param productName 产品名称
     */
    public void createBucket(String productName) {
        MinioClient minioClient = getMinioClient(productName);
        if (Objects.nonNull(minioClient)) {
            try {
                if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(PRODUCT_MAP.get(productName).getBucketName()).build())) {
                    minioClient.makeBucket(MakeBucketArgs.builder().bucket(PRODUCT_MAP.get(productName).getBucketName()).build());
                }
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                     InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                     XmlParserException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 获取文件外链
     *
     * @param productName 产品名称
     * @param objectName  文件名
     * @return 响应
     */
    public String getObjectURL(String productName, String objectName) {
        MinioClient minioClient = getMinioClient(productName);
        if (Objects.nonNull(minioClient)) {
            try {
                return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(PRODUCT_MAP.get(productName).getBucketName()).object(objectName).build());
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                     InvalidResponseException | IOException | NoSuchAlgorithmException | XmlParserException |
                     ServerException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }

    /**
     * 获取文件
     *
     * @param productName 产品名称
     * @param objectName  文件名称
     * @param response    响应体
     * @return 错误信息
     */
    public String getObject(String productName, String objectName, HttpServletResponse response) {
        MinioClient minioClient = getMinioClient(productName);
        String msg = "";
        if (Objects.nonNull(minioClient)) {
            ServletOutputStream data = null;
            InputStream is = null;
            try {
                // 读取文件内容。
                is = minioClient.getObject(GetObjectArgs.builder().bucket(PRODUCT_MAP.get(productName).getBucketName()).object(objectName).build());
                data = response.getOutputStream();
                // 5M一次读写
                byte[] buffer = new byte[1024 * 1024 * 5];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    data.write(buffer, 0, len);
                }
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                     InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                     XmlParserException e) {
                msg = "下载文件异常：" + e.getMessage();
                log.error("下载文件异常：{}", e.getMessage(), e);
            } finally {
                if (null != is) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        log.error("流关闭异常:[{}]", objectName);
                    }
                }
                if (null != data) {
                    try {
                        data.close();
                    } catch (IOException e) {
                        log.error("字节数组流关闭异常:[{}]", objectName);
                    }
                }
            }
        }
        return msg;
    }

    /**
     * 上传文件
     *
     * @param productName 产品名称
     * @param objectName  文件名称
     * @param stream      文件
     */
    public Boolean putObject(String productName, String objectName, InputStream stream) {
        createBucket(productName);
        MinioClient minioClient = getMinioClient(productName);
        if (Objects.nonNull(minioClient)) {
            try {
                putObject(productName, objectName, stream, stream.available(), "application/octet-stream");
                return true;
            } catch (IOException e) {
                log.error("MinIO上传文件异常:{}", e.getMessage(), e);
                return false;
            }
        }
        return false;
    }

    /**
     * 上传文件
     *
     * @param productName 产品名称
     * @param objectName  文件名称
     * @param stream      文件
     * @param size        文件大小
     * @param contextType 文件类型
     */
    public void putObject(String productName, String objectName, InputStream stream, int size, String contextType) {
        createBucket(productName);
        MinioClient minioClient = getMinioClient(productName);
        if (Objects.nonNull(minioClient)) {
            try {
                minioClient.putObject(PutObjectArgs.builder().bucket(PRODUCT_MAP.get(productName).getBucketName()).object(objectName).stream(stream, size, 104857600).contentType(contextType).build());
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                     InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                     XmlParserException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 删除文件
     *
     * @param productName 产品名称
     * @param objectName  文件名称
     */
    public void deleteFile(String productName, String objectName) {
        MinioClient minioClient = getMinioClient(productName);
        if (Objects.nonNull(minioClient)) {
            try {
                minioClient.removeObject(RemoveObjectArgs.builder().bucket(PRODUCT_MAP.get(productName).getBucketName()).object(objectName).build());
            } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                     InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                     XmlParserException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 获取文件地址
     *
     * @param productName 产品名称
     * @param name        文件名称
     * @return 文件地址
     */
    public String getFileUrl(String productName, String name) {
        ProductEntity productEntity = PRODUCT_MAP.get(productName);
        return "http://" + productEntity.getEndpoint() + ":" + MIN_IO_PORT + "/" + productEntity.getBucketName() + "/" + name;
    }

    public FilePartUploadResponse uploadChunk(ProductEnum productEnum, String fileName, FilePartUploadRequest request, MultipartFile file) {
        if (StringUtils.isEmpty(fileName)) {
            throw new APIRuntimeException(ResultCodeEnum.UPLOAD_FILE_NAME_ERROR);
        }
        MinioClient minioClient = getMinioClient(productEnum.getName());
        if (Objects.isNull(minioClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            // 判断是否上传
            if (fileExists(productEnum, fileName)) {
                FilePartUploadResponse response = new FilePartUploadResponse();
                response.setSkipUpload(true);
                response.setUrl(getFileUrl(productEnum.getName(), fileName));
                return response;
            }
            // 判断是否第一次上传
            if (StringUtils.isBlank(request.getUploadId())) {
                request.setUploadId(uploadChunkInit(productEnum));
                FilePartUploadResponse response = new FilePartUploadResponse();
                response.setSkipUpload(false);
                response.setUploadId(request.getUploadId());
                response.setUploaded(null);
                return response;
            }

            // 检查分片是否已上传 实现断点续传
            if (file == null) {
                Map<Object, Object> uploadedCache = redisRepository.hmGetAll(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId());
                List<Integer> uploaded = Lists.newArrayList();

                for (Map.Entry<Object, Object> entry : uploadedCache.entrySet()) {
                    uploaded.add(JSON.parseObject(entry.getValue().toString(), PartETag.class).getPartNumber());
                }
                FilePartUploadResponse response = new FilePartUploadResponse();
                response.setSkipUpload(false);
                response.setUploadId(request.getUploadId());
                response.setUploaded(uploaded);
                return response;
            }
            // 上传分片
            PartETag partETag = uploadPartFile(productEnum, request.getUploadId(), file.getInputStream(), request.getChunkNumber());
            if (Objects.isNull(partETag)) {
                log.error("分片上传失败 上传响应为空");
                throw new APIRuntimeException(ResultCodeEnum.UPLOAD_TAG_FAIL);
            }
            // 分片上传完成缓存key
            redisRepository.hmSet(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId(), request.getChunkNumber() + ",", JSON.toJSONString(partETag));
            // 取出所有已上传的分片信息
            Map<Object, Object> dataMap = redisRepository.hmGetAll(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId());
            List<PartETag> partETagList = Lists.newArrayList();
            for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
                partETagList.add(JSON.parseObject(entry.getValue().toString(), PartETag.class));
            }
            FilePartUploadResponse response = new FilePartUploadResponse();
            // 判断是否上传完成
            if (dataMap.keySet().size() == request.getTotalChunks()) {
                StatObjectResponse statObjectResponse = uploadChunkComplete(productEnum, request.getUploadId(), fileName, partETagList);
                for (Object mapKey : dataMap.keySet()) {
                    redisRepository.hdel(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId(), mapKey.toString());
                }

                response.setSkipUpload(true);
                response.setUploadId(request.getUploadId());
                response.setUrl(getFileUrl(productEnum.getName(), fileName));
                response.setFinish(true);
                response.setSize(statObjectResponse.size());
            } else {
                List<Integer> list = partETagList.stream().map(PartETag::getPartNumber).collect(Collectors.toList());
                response.setUploaded(list);
                response.setSkipUpload(false);
                response.setUploadId(request.getUploadId());
            }
            return response;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传失败，请检查文件正确性", e);
            throw new APIRuntimeException(ResultCodeEnum.UPLOAD_FAIL);
        }
    }

    /**
     * 分片上传文件
     *
     * @param productEnum 产品名称
     * @param uploadId    上传id
     * @param inputStream 文件
     * @param chunkNumber 当前片
     * @return 上传片的tag
     */
    private PartETag uploadPartFile(ProductEnum productEnum, String uploadId, InputStream inputStream, Integer chunkNumber) {
        MinioClient minioClient = getMinioClient(productEnum.getName());
        if (Objects.isNull(minioClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(uploadId)
                    .object(chunkNumber.toString())
                    .stream(inputStream, inputStream.available(), -1)
                    .build());
            return new PartETag(chunkNumber, chunkNumber.toString());
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 完成分片上传
     *
     * @param productEnum  产品
     * @param uploadId     上传id
     * @param fileName     文件名
     * @param partETagList 分片列表
     */
    private StatObjectResponse uploadChunkComplete(ProductEnum productEnum, String uploadId, String fileName, List<PartETag> partETagList) {
        MinioClient minioClient = getMinioClient(productEnum.getName());
        if (Objects.isNull(minioClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        List<ComposeSource> sources = new ArrayList<>();
        // 最后一片放在最后
        partETagList.sort((o1, o2) -> Integer.max(o2.getPartNumber(), o1.getPartNumber()));
        for (PartETag partETag : partETagList) {
            sources.add(ComposeSource.builder().bucket(uploadId).object(String.valueOf(partETag.getPartNumber())).build());
        }
        try {
            minioClient.composeObject(ComposeObjectArgs.builder()
                    .bucket(PRODUCT_MAP.get(productEnum.getName()).getName())
                    .object(fileName)
                    .sources(sources)
                    .build());
            return getFileInfo(minioClient, PRODUCT_MAP.get(productEnum.getName()).getName(), fileName);
        } catch (Exception e) {
            throw new APIRuntimeException(ResultCodeEnum.CHUNK_COMPLETE_FAIL);
        }
    }

    /**
     * 获取文件信息
     */
    private StatObjectResponse getFileInfo(MinioClient minioClient, String bucketName, String fileName) throws Exception {
        return minioClient.statObject(StatObjectArgs.builder()
                .bucket(bucketName)
                .object(fileName)
                .build());
    }

    /**
     * 判断文件是否存在
     *
     * @param productEnum 产品
     * @param fileName    文件名
     * @return 文件是否存在
     */
    private boolean fileExists(ProductEnum productEnum, String fileName) {

        MinioClient minioClient = getMinioClient(productEnum.getName());
        if (Objects.isNull(minioClient)) {
            return false;
        }
        ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
        // Object是否存在。
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(productEntity.getBucketName()).object(fileName).build());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 初始化文件
     *
     * @param productEnum 产品
     * @return 本次上传的随机id
     */
    private String uploadChunkInit(ProductEnum productEnum) {
        if (Objects.isNull(productEnum)) {
            throw new APIRuntimeException(ResultCodeEnum.KEY_NOT_NULL);
        }
        MinioClient minioClient = getMinioClient(productEnum.getName());
        if (Objects.isNull(minioClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            String uploadId = UUID.randomUUID().toString().replaceAll("-", "");
            makeBucket(productEnum, uploadId);
            return uploadId;
        } catch (Exception e) {
            e.printStackTrace();
            throw new APIRuntimeException(ResultCodeEnum.CHUNK_INIT_FAIL);
        }
    }

    /**
     * 创建bucket
     *
     * @param productEnum 产品名称
     * @param bucketName  要创建的bucket名称
     */
    public void makeBucket(ProductEnum productEnum, String bucketName) {
        if (!bucketExists(productEnum, bucketName)) {
            MinioClient minioClient = getMinioClient(productEnum.getName());
            if (Objects.isNull(minioClient)) {
                throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
            }
            try {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            } catch (Exception e) {
                throw new APIRuntimeException(ResultCodeEnum.CHUNK_INIT_FAIL);
            }
        }
    }

    /**
     * 校验bucket是否存在
     *
     * @param productEnum 产品
     * @param bucketName  bucket名称
     * @return 是否存在
     */
    private boolean bucketExists(ProductEnum productEnum, String bucketName) {
        MinioClient minioClient = getMinioClient(productEnum.getName());
        if (Objects.isNull(minioClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            return false;
        }
    }
}